﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Data.Schema.ScriptDom.Sql;
using System.Globalization;

namespace SQLite2CELib
{
    internal class FromClauseConverter
    {
        private string _tableNotFoundErrorMsg = "Converter could not find table: {0}. Either the table name is entered incorrectly or it is a view on another view.";
        private string _linqQueryFromClause = "from each{0} in context.{1}";
        private string _linqQueryInnerJoinClause = "join each{0} in context.{0} on  new {{ {1} }} equals  new {{ {2} }} ";
        private string _linqQueryInnerJoinCondition = "Condition{0} = {1},"; //0 = index of condition, 1 = Expression
        /*
        * 0 = Entity Class Name, 1 = LHS Exp, 2 = RHS Exp
        */
        private const string _linqQueryLeftJoinClause = "join each{0} in context.{0} on new {{ {1} }} equals new {{ {2} }} into leftJoin{0}Results from eachleftJoin{0}Results in leftJoin{0}Results.DefaultIfEmpty()";

        private TableSchemaHelper _schemaHelper;
        private Utils _utils;

        private StringBuilder _fromClause;
        private List<String> _typesInQuery;
        private Dictionary<String, String> _entityNameAndLeftJoinedEntityName;
        private Dictionary<String, String> _tableAliasAndTableName;

        public FromClauseConverter(TableSchemaHelper schemaHelper, Utils utils)
        {
            _schemaHelper = schemaHelper;
            _utils = utils;
            _tableAliasAndTableName = new Dictionary<string, string>();
        }

        public Dictionary<String, String> TableNameAndAliasInfo
        {
            get
            {
                return _tableAliasAndTableName;
            }
        }

        public StringBuilder Convert(IList<TableSource> fromClause, ref List<String> tablesInQuery, ref Dictionary<String, String> entityNameAndLeftJoinedEntityName)
        {
            _typesInQuery = tablesInQuery;
            _entityNameAndLeftJoinedEntityName = entityNameAndLeftJoinedEntityName;

            return ExtractFromClause(fromClause);
        }

        private StringBuilder ExtractFromClause(IList<TableSource> fromclauses)
        {
            _fromClause = new StringBuilder();

            foreach (TableSource eachTableSource in fromclauses)
            {
                if (eachTableSource is SchemaObjectTableSource)
                {
                    ProcessSimpleFromClause(eachTableSource as SchemaObjectTableSource, ref _fromClause);
                }
                else if (eachTableSource is QualifiedJoin)
                {
                    ProcessJoinClause(eachTableSource as QualifiedJoin, ref _fromClause);
                }
            }
            return _fromClause;
        }

        private void ProcessSimpleFromClause(SchemaObjectTableSource fromClause, ref StringBuilder linqFromClause)
        {
            String tableName;
            String typeNameForTableName;

            //Type name are not same as that of table name for scenarios where table name have spaces in between them.
            tableName = fromClause.SchemaObject.Identifiers[0].Value;
            UpdateTableAliasInfo(fromClause, tableName);
            typeNameForTableName = _schemaHelper.GetClassNameForTableName(tableName);

            if (String.IsNullOrEmpty(typeNameForTableName) == true)
            {
                throw new NotSupportedException(String.Format(CultureInfo.InvariantCulture, _tableNotFoundErrorMsg, tableName));
            }

            _typesInQuery.Add(typeNameForTableName);

            linqFromClause.AppendFormat(_linqQueryFromClause, typeNameForTableName, typeNameForTableName);
            linqFromClause.Append(Environment.NewLine);
        }

        private void ProcessJoinClause(QualifiedJoin joinClause, ref StringBuilder linqFromClause)
        {
            SchemaObjectTableSource firstTableSource, secondTableSource;
            StringBuilder lhsExp, rhsExp;
            String typeNameForTableName;
            String tableName;

            if (joinClause == null)
            {
                return; //Invalid input.
            }

            if (joinClause.FirstTableSource is SchemaObjectTableSource)
            {
                firstTableSource = joinClause.FirstTableSource as SchemaObjectTableSource;
                tableName = firstTableSource.SchemaObject.Identifiers[0].Value;
                UpdateTableAliasInfo(firstTableSource, tableName);
                typeNameForTableName = _schemaHelper.GetClassNameForTableName(_tableAliasAndTableName.ContainsKey(tableName) == true ?
                    _tableAliasAndTableName[tableName] : tableName);

                if (String.IsNullOrEmpty(typeNameForTableName) == true)
                {
                    throw new NotSupportedException(String.Format(CultureInfo.InvariantCulture, _tableNotFoundErrorMsg, tableName));
                }

                _typesInQuery.Add(typeNameForTableName);

                if (linqFromClause.Length == 0)
                {
                    linqFromClause.AppendFormat(_linqQueryFromClause, typeNameForTableName, typeNameForTableName);
                    linqFromClause.Append(Environment.NewLine);
                }
                else
                {
                    lhsExp = new StringBuilder();
                    rhsExp = new StringBuilder();
                    ExtractJoinExpression(joinClause.SearchCondition as BinaryExpression, typeNameForTableName, ref lhsExp, ref rhsExp);
                    //Remove extra comma.
                    RemoveCommaFromEndOfStr(ref lhsExp);
                    RemoveCommaFromEndOfStr(ref rhsExp);
                    linqFromClause.AppendFormat(_linqQueryInnerJoinClause, typeNameForTableName, lhsExp.ToString(), rhsExp.ToString());
                }
            }
            else
            {
                ProcessJoinClause(joinClause.FirstTableSource as QualifiedJoin, ref linqFromClause);
            }

            if (joinClause.SecondTableSource is SchemaObjectTableSource)
            {
                secondTableSource = joinClause.SecondTableSource as SchemaObjectTableSource;
                tableName = secondTableSource.SchemaObject.Identifiers[0].Value;
                UpdateTableAliasInfo(secondTableSource, tableName);
                typeNameForTableName = _schemaHelper.GetClassNameForTableName(_tableAliasAndTableName.ContainsKey(tableName) == true ?
                    _tableAliasAndTableName[tableName] : tableName);

                if (String.IsNullOrEmpty(typeNameForTableName) == true)
                {
                    throw new NotSupportedException(String.Format(CultureInfo.InvariantCulture, _tableNotFoundErrorMsg, tableName));
                }

                _typesInQuery.Add(typeNameForTableName);

                if (linqFromClause.Length == 0)
                {
                    linqFromClause.AppendFormat(_linqQueryFromClause, typeNameForTableName, typeNameForTableName);
                    linqFromClause.Append(Environment.NewLine);
                    return;
                }

                lhsExp = new StringBuilder();
                rhsExp = new StringBuilder();
                ExtractJoinExpression(joinClause.SearchCondition as BinaryExpression, typeNameForTableName, ref lhsExp, ref rhsExp);
                RemoveCommaFromEndOfStr(ref lhsExp);
                RemoveCommaFromEndOfStr(ref rhsExp);
                linqFromClause.Append(PrepareJoinClause(joinClause, typeNameForTableName, lhsExp, rhsExp));
                linqFromClause.Append(Environment.NewLine);
            }
            else
            {
                ProcessJoinClause(joinClause.SecondTableSource as QualifiedJoin, ref linqFromClause);
            }
        }

        private String PrepareJoinClause(QualifiedJoin joinClause, String typeNameForTableName, StringBuilder lhsExp, StringBuilder rhsExp)
        {
            String linqJoinClause;

            linqJoinClause = String.Empty;
            switch (joinClause.QualifiedJoinType)
            {
                default:
                case QualifiedJoinType.FullOuter:
                    throw new Exception("Not Supported");
                case QualifiedJoinType.Inner:
                    {
                        linqJoinClause = String.Format(CultureInfo.InvariantCulture, _linqQueryInnerJoinClause, typeNameForTableName, lhsExp, rhsExp);
                        break;
                    }
                case QualifiedJoinType.LeftOuter:
                    {
                        UpdateLeftJoinEntityNameMapping(typeNameForTableName, String.Format(CultureInfo.InvariantCulture, "leftJoin{0}Results", typeNameForTableName));
                        linqJoinClause = String.Format(CultureInfo.InvariantCulture, _linqQueryLeftJoinClause, typeNameForTableName, lhsExp, rhsExp);
                        break;
                    }
                case QualifiedJoinType.RightOuter:
                    throw new Exception("Not Supported");
            }

            return linqJoinClause;
        }

        private void ExtractJoinExpression(BinaryExpression searchCondition, string rhsTableName, ref StringBuilder lhsExp, ref StringBuilder rhsExp, int conditionIndex = 0)
        {
            KeyValuePair<String, PropertyDetails> matchingClassNameAndTypeInfoForLhsExp, matchingClassNameAndTypeInfoForRhsExp;
            Object lhsExpVal, rhsExpVal, tempExpVal;
            Expression lhsExpression, rhsExpression;

            if (_utils.IsExpressionCondition(searchCondition) == true)
            {
                //Make sure that if we have constant in our expression it is always in right..This will help us to ensure right data types in expressions.
                _utils.EnsureLhsExpIsColumn(searchCondition.FirstExpression, searchCondition.SecondExpression, out lhsExpression, out rhsExpression);
                lhsExpVal = _utils.ExtractValueFromExpression(lhsExpression, out matchingClassNameAndTypeInfoForLhsExp, null, true);

                //If we have constants on right expression then we should send type information about LHS so that if RHS is constant
                //expressions are generated in right way.
                rhsExpVal = _utils.ExtractValueFromExpression(rhsExpression, out matchingClassNameAndTypeInfoForRhsExp, matchingClassNameAndTypeInfoForLhsExp.Value.TypeInfo, true);

                //Check if rhsExpVal is meant for rhsTable.. otherwise LINQ wont compile.
                //For example In SQL following query is valid ..
                /*
                 * SELECT * FROM Customers
                 * JOIN Orders ON Order.CustomerId = Customers.CustomerId -- Note that LHS of JOIN EXP is for Orders table..
                 * BUT This is not allowed in LINQ. SO following linq is invalid
                 * for eachCustomer in context.Customers
                 * JOIN eachOrder in context.Orders ON new { eachOrder.CustomerId } equals new { eachCustomer.CustomerId }
                 * It should be following
                 * for eachCustomer in context.Customers
                 * JOIN eachOrder in context.Orders ON new { eachCustomer.CustomerId } equals new { eachOrder.CustomerId }
                 */
                if (lhsExpVal.ToString().Contains("each" + rhsTableName) == true)
                {
                    //Swap rhs with lhs.
                    tempExpVal = lhsExpVal;
                    lhsExpVal = rhsExpVal;
                    rhsExpVal = tempExpVal;
                }
                lhsExp.AppendFormat(_linqQueryInnerJoinCondition, conditionIndex, lhsExpVal);
                rhsExp.AppendFormat(_linqQueryInnerJoinCondition, conditionIndex, rhsExpVal);
                return;
            }

            if (searchCondition.FirstExpression is BinaryExpression)
            {
                ExtractJoinExpression(searchCondition.FirstExpression as BinaryExpression, rhsTableName, ref lhsExp, ref rhsExp, conditionIndex++);
            }
            if (searchCondition.SecondExpression is BinaryExpression)
            {
                ExtractJoinExpression(searchCondition.SecondExpression as BinaryExpression, rhsTableName, ref lhsExp, ref rhsExp, conditionIndex++);
            }
        }

        private void UpdateLeftJoinEntityNameMapping(String entityName, String leftJoinedEntityName)
        {
            _entityNameAndLeftJoinedEntityName.Add(entityName, leftJoinedEntityName);
        }

        private void RemoveCommaFromEndOfStr(ref StringBuilder stringWithComma)
        {
            stringWithComma.Remove(stringWithComma.Length - 1, 1);
        }

        private void UpdateTableAliasInfo(SchemaObjectTableSource tableSource, String tableName)
        {
            if (tableSource.Alias != null)
            {
                _tableAliasAndTableName.Add(tableSource.Alias.Value, tableName);
            }
        }
    }
}
